// // Copyright (c) 2009 All Right Reserved // // Stephen Toub // stoub@microsoft.com // 2009-01-01 // Contains ... // Classes for parsing track data into actual MIDI track and event objects using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Text; using LargoCommon.Music; namespace LargoCommon.Midi { /// MIDI track parser. /// was internal, added sealed public sealed class MidiParser { /// /// SysEx Continue. /// private static bool sysExContinue; /// /// Data to parse. /// private readonly byte[] data; /// /// SysEx data to parse. /// private byte[] sysExData; #region Constructors /// /// Initializes a new instance of the class. /// /// The given data. /// The given sys ex data. public MidiParser(byte[] givenData, byte[] givenSysExData) { Contract.Requires(givenData != null); //// if (givenData == null) { throw new InvalidOperationException("Empty Midi data to parse"); } this.data = givenData; this.sysExData = givenSysExData; } #endregion /// /// Parses a byte array into a track's worth of events. /// /// /// The track containing the parsed events. /// public MidiTrack ParseToTrack() { //// cyclomatic complexity 10:15 var pos = 0; // current position in data var status = 0; // the current status byte sysExContinue = false; // whether we're in a multi-segment system exclusive message try { // Create the new track var track = new MidiTrack(); // Process all bytes, turning them into events while (pos >= 0 && pos < this.data.Length) { //// // Read in the delta time long deltaTime = ReadVariableLength(this.data, ref pos); var running = this.GetRunningStatus(pos, ref status); var tempEvent = this.ParseDataToEvent(deltaTime, ref pos, status, running); if (tempEvent != null) { //// Add the newly parsed event if we got one track.Events.Add(tempEvent); } } //// Return the newly populated track return track; } catch (OverflowException exc) { //// Wrap all other exceptions in MidiParserExceptions throw new MidiParserException("Failed to parse MIDI file (Overflow).", exc, pos); } catch (OutOfMemoryException exc) { //// Wrap all other exceptions in MidiParserExceptions throw new MidiParserException("Failed to parse MIDI file (Out of memory).", exc, pos); } catch (ArithmeticException exc) { //// Wrap all other exceptions in MidiParserExceptions throw new MidiParserException("Failed to parse MIDI file (Arithmetic exception).", exc, pos); } //// Let MidiParserExceptions through //// catch (MidiParserException) { throw; } } /// /// Parse TextEvent. /// /// The previously parsed delta-time for this event. /// The previously parsed type of message we're expecting to find. /// The data stream from which to read the event information. /// The position of the start of the event information. /// Returns value. private static MidiEvent ParseTextEvent(long deltaTime, MidiMetaType eventType, byte[] data, ref int pos) { //// cyclomatic complexity 10:12 Contract.Requires(data != null); Contract.Requires(pos > (long)0); Contract.Requires(pos < data.Length); //// if (data == null) { return null; } MidiEvent tempEvent = null; if (!(eventType == MidiMetaType.TextEvent || eventType == MidiMetaType.CopyrightNotice || eventType == MidiMetaType.SoundtrackName || eventType == MidiMetaType.TrackInstrumentName || eventType == MidiMetaType.Lyric || eventType == MidiMetaType.Marker || eventType == MidiMetaType.CuePoint || eventType == MidiMetaType.ProgramName || eventType == MidiMetaType.DeviceName)) { return null; } var text = ReadAsciiText(data, ref pos); switch (eventType) { //// Text events (copyright, lyrics, etc) case MidiMetaType.TextEvent: tempEvent = new MetaText(deltaTime, text); break; case MidiMetaType.CopyrightNotice: tempEvent = new MetaCopyright(deltaTime, text); break; case MidiMetaType.SoundtrackName: tempEvent = new MetaSequenceTrackName(deltaTime, text); break; case MidiMetaType.TrackInstrumentName: tempEvent = new MetaInstrument(deltaTime, text); break; case MidiMetaType.Lyric: tempEvent = new MetaLyric(deltaTime, text); break; case MidiMetaType.Marker: tempEvent = new MetaMarker(deltaTime, text); break; case MidiMetaType.CuePoint: tempEvent = new MetaCuePoint(deltaTime, text); break; case MidiMetaType.ProgramName: tempEvent = new MetaProgramName(deltaTime, text); break; case MidiMetaType.DeviceName: tempEvent = new MetaDeviceName(deltaTime, text); break; case MidiMetaType.TrackSequenceNumber: break; case MidiMetaType.MidiChannelPrefix: break; case MidiMetaType.MidiPort: break; case MidiMetaType.MetaEndOfTrack: break; case MidiMetaType.SetTempo: break; case MidiMetaType.TimeCodeOffset: break; case MidiMetaType.TimeSignature: break; case MidiMetaType.KeySignature: break; case MidiMetaType.SequencerSpecific: break; //// resharper default: break; } return tempEvent; } /// /// Parse UnknownEvent. /// /// The previously parsed delta-time for this event. /// The previously parsed type of message we're expecting to find. /// The data stream from which to read the event information. /// The position of the start of the event information. /// Returns value. private static MidiEvent ParseUnknownEvent(long deltaTime, byte eventType, byte[] data, ref int pos) { Contract.Requires(data != null); if (data == null || pos < 0) { return null; } //// Read in the variable length and that much data, then store it var length = ReadVariableLength(data, ref pos); if (length < 0 || pos < data.GetLowerBound(0) || pos + length > data.GetLowerBound(0) + data.Length) { return null; } var unknownData = new byte[length]; Array.Copy(data, pos, unknownData, 0, length); MidiEvent tempEvent = new MetaUnknown(deltaTime, eventType, unknownData); pos += length; return tempEvent; } /// Parse a meta MIDI event from the data stream. /// The previously parsed delta-time for this event. /// The previously parsed type of message we're expecting to find. /// The data stream from which to read the event information. /// The position of the start of the event information. /// The parsed meta MIDI event. private static MidiEvent ParseMetaEvent(long deltaTime, byte eventType, byte[] data, ref int pos) { //// cyclomatic complexity 10:16 Contract.Requires(data != null); Contract.Requires(pos >= (long)0); Contract.Requires(pos + 1 < data.Length); //// if (data == null) { return null; } try { MidiEvent tempEvent = null; if (eventType >= (byte)MidiMetaType.TextEvent && eventType <= (byte)MidiMetaType.DeviceName) { //// 0x01 to 0x09 tempEvent = ParseTextEvent(deltaTime, (MidiMetaType)eventType, data, ref pos); return tempEvent; } //// Create the correct meta event based on its meta event id/type switch ((MidiMetaType)eventType) { //// Sequence number case MidiMetaType.TrackSequenceNumber: tempEvent = GetSequenceNumberEvent(deltaTime, data, ref pos); break; //// Channel prefix case MidiMetaType.MidiChannelPrefix: pos++; // skip 0x1 if (pos >= 0 && pos < data.Length) { tempEvent = new MetaChannelPrefix(deltaTime, data[pos]); } pos++; // skip read value break; //// Port number case MidiMetaType.MidiPort: pos++; // skip 0x1 if (pos >= 0 && pos < data.Length) { tempEvent = new MetaPort(deltaTime, data[pos]); } pos++; //// skip read value break; //// Key signature case MidiMetaType.KeySignature: pos++; // skip past 0x2 if (pos >= 0 && pos + 1 < data.Length) { tempEvent = new MetaKeySignature(deltaTime, (TonalityKey)data[pos], (TonalityGenus)data[pos + 1]); } pos += 2; break; //// End of track case MidiMetaType.MetaEndOfTrack: pos++; //// skip 0x0 tempEvent = new MetaEndOfTrack(deltaTime); break; //// Tempo case MidiMetaType.SetTempo: tempEvent = GetTempoEvent(deltaTime, data, ref pos); break; //// SMPTE offset case MidiMetaType.TimeCodeOffset: tempEvent = GetTimeCodeOffsetEvent(deltaTime, data, ref pos); break; //// Time signature case MidiMetaType.TimeSignature: tempEvent = GetTimeSignatureEvent(deltaTime, data, ref pos); break; //// Proprietary case MidiMetaType.SequencerSpecific: tempEvent = GetSequencerSpecificEvent(deltaTime, data, ref pos); break; //// An unknown meta event! default: tempEvent = ParseUnknownEvent(deltaTime, eventType, data, ref pos); break; } return tempEvent; } catch (OverflowException exc) { throw new MidiParserException("Unable to parse meta MIDI event (Overflow).", exc, pos); } catch (OutOfMemoryException exc) { throw new MidiParserException("Unable to parse meta MIDI event (Out of memory).", exc, pos); } catch (ArithmeticException exc) { throw new MidiParserException("Unable to parse meta MIDI event (Arithmetic exception).", exc, pos); } } #region Reading Helpers /// Reads an ASCII text sequence from the data stream. /// The data stream from which to read the text. /// The position of the start of the sequence. /// The text as a string. private static string ReadAsciiText(byte[] data, ref int pos) { Contract.Requires(data != null); Contract.Requires(pos >= (long)0); Contract.Requires(pos < data.Length); //// Read the length of the string, grab the following data as ASCII text, move ahead var length = ReadVariableLength(data, ref pos); //// 2013/03 var text = data.Length >= pos + length ? Encoding.ASCII.GetString(data, pos, length) : string.Empty; pos += length; return text; } /// Reads a variable-length value from the data stream. /// The data to process. /// The position at which to start processing. /// The value read; position is updated to reflect the new position. private static int ReadVariableLength(IList data, ref int pos) { Contract.Requires(data != null); if (pos < 0 || pos >= data.Count) { return 0; } //// Start with the first byte int length = data[pos]; //// If the special "there's more data" marker isn't set, we're done if ((data[pos] & 0x80) != 0) { // Remove the special marker length &= 0x7f; do { //// Continually get all bytes, removing the marker, until no marker is found pos++; if (pos < data.Count) { length = (length << 7) + (data[pos] & 0x7f); } } while (pos < data.Count && ((data[pos] & 0x80) != 0)); } // Advance past the last used byte and return the length pos++; return length; } #endregion #region Particular Meta Events /// /// Gets the tempo event. /// /// The delta time. /// The given data. /// The position. /// Returns value. private static MidiEvent GetTempoEvent(long deltaTime, IList data, ref int pos) { Contract.Requires(data != null); const byte eventLength = 3; pos++; // skip 0x3 var tempo = pos >= 0 && pos + 2 < data.Count ? (data[pos] << 16) | data[pos + 1] << 8 | data[pos + 2] : 0; MidiEvent tempEvent = new MetaTempo(deltaTime, tempo); pos += eventLength; return tempEvent; } /// /// Gets the time code offset event. /// /// The delta time. /// The given data. /// The position. /// Returns value. private static MidiEvent GetTimeCodeOffsetEvent(long deltaTime, IList data, ref int pos) { Contract.Requires(data != null); const byte eventLength = 5; pos++; // skip 0x5 MidiEvent tempEvent = pos >= 0 && pos + 4 < data.Count ? new MetaTimeCodeOffset(deltaTime, data[pos], data[pos + 1], data[pos + 2], data[pos + 3], data[pos + 4]) : null; pos += eventLength; return tempEvent; } /// /// Gets the time signature event. /// /// The delta time. /// The given data. /// The position. /// Returns value. private static MidiEvent GetTimeSignatureEvent(long deltaTime, IList data, ref int pos) { Contract.Requires(data != null); const byte eventLength = 4; pos++; // skip past 0x4 MidiEvent tempEvent = pos >= 0 && pos + 3 < data.Count ? new MetaTimeSignature(deltaTime, data[pos], data[pos + 1], data[pos + 2], data[pos + 3]) : null; pos += eventLength; return tempEvent; } /// /// Gets the sequencer specific event. /// /// The delta time. /// The given data. /// The position. /// Returns value. private static MidiEvent GetSequencerSpecificEvent(long deltaTime, byte[] data, ref int pos) { Contract.Requires(data != null); //// Read in the variable length and that much data, then store it var length = ReadVariableLength(data, ref pos); if (length < 0 || pos < data.GetLowerBound(0) || pos + length > data.GetLowerBound(0) + data.Length) { return null; } var propData = new byte[length]; Array.Copy(data, pos, propData, 0, length); MidiEvent tempEvent = new MetaProprietary(deltaTime, propData); pos += length; return tempEvent; } /// /// Gets the sequence number event. /// /// The delta time. /// The given data. /// The position. /// Returns value. private static MidiEvent GetSequenceNumberEvent(long deltaTime, IList data, ref int pos) { Contract.Requires(data != null); const byte eventLength = 2; MidiEvent tempEvent = null; pos++; //// skip past the 0x02 if (pos >= 0 && pos + 1 < data.Count) { var number = (data[pos] << 8) | data[pos + 1]; tempEvent = new MetaSequenceNumber(deltaTime, number); } pos += eventLength; //// skip read values return tempEvent; } /// Parse a voice event from the data stream. /// The previously parsed delta-time for this event. /// The previously parsed type of message we're expecting to find. /// The previously parsed channel for this message. /// The data stream from which to read the event information. /// The position of the start of the event information. /// The parsed voice MIDI event. private static MidiEvent ParseVoiceEvent( long deltaTime, MidiVoiceMessageType messageType, MidiChannel channel, IList data, ref int pos) { //// cyclomatic complexity 10:16 Contract.Requires(data != null); Contract.Requires(pos + 1 < data.Count); //// if (data == null) { return null; } try { MidiEvent tempEvent = null; //// Create the correct voice event based on its message id/type switch (messageType) { case MidiVoiceMessageType.VoiceNoteOff: if (pos >= 0 && pos + 1 < data.Count) { tempEvent = new VoiceNoteOff(deltaTime, channel, data[pos], data[pos + 1]); } pos += 2; break; case MidiVoiceMessageType.VoiceNoteOn: if (pos >= 0 && pos + 1 < data.Count) { tempEvent = new VoiceNoteOn(deltaTime, channel, data[pos], data[pos + 1]); } pos += 2; break; case MidiVoiceMessageType.PolyphonicKeyPressure: if (pos >= 0 && pos + 1 < data.Count) { tempEvent = new VoiceAftertouch(deltaTime, channel, data[pos], data[pos + 1]); } pos += 2; break; case MidiVoiceMessageType.ControllerChange: if (pos >= 0 && pos + 1 < data.Count) { tempEvent = new VoiceController(deltaTime, channel, data[pos], data[pos + 1]); } pos += 2; break; case MidiVoiceMessageType.ProgramChange: if (pos >= 0 && pos < data.Count) { tempEvent = new VoiceProgramChange(deltaTime, channel, data[pos]); } pos += 1; break; case MidiVoiceMessageType.ChannelKeyPressure: if (pos >= 0 && pos < data.Count) { tempEvent = new VoiceChannelPressure(deltaTime, channel, data[pos]); } pos += 1; break; case MidiVoiceMessageType.PitchBend: tempEvent = GetPitchBendEvent(deltaTime, channel, data, ref pos); break; //// UH OH! default: throw new ArgumentOutOfRangeException(nameof(messageType), messageType, "Not a voice message."); } //// Return the newly parsed event return tempEvent; } catch (OverflowException exc) { throw new MidiParserException("Unable to parse meta MIDI event (Overflow).", exc, pos); } catch (OutOfMemoryException exc) { throw new MidiParserException("Unable to parse meta MIDI event (Out of memory).", exc, pos); } catch (ArithmeticException exc) { throw new MidiParserException("Unable to parse meta MIDI event (Arithmetic exception).", exc, pos); } } /// /// Gets the pitch bend event. /// /// The delta time. /// The channel. /// The given data. /// The position. /// /// Returns value. /// private static MidiEvent GetPitchBendEvent(long deltaTime, MidiChannel channel, IList data, ref int pos) { Contract.Requires(data != null); const byte eventLength = 2; MidiEvent tempEvent = null; if (pos >= 0 && pos + 1 < data.Count) { var position = (data[pos] << 8) | data[pos + 1]; MidiEvent.Split14BitsToBytes(position, out var upper, out var lower); tempEvent = new VoicePitchWheel(deltaTime, channel, upper, lower); } pos += eventLength; return tempEvent; } #endregion #region High-Level Parsing /// /// Gets the running status. /// /// The position. /// The status. /// Returns value. private bool GetRunningStatus(int pos, ref int status) { //// // Get the next character var nextValue = (byte)(pos >= 0 && pos < this.data.Length ? this.data[pos] : 0); //// // Are we continuing a sys ex? If so, the next value better be 0x7F if (sysExContinue && (nextValue != 0x7f)) { throw new MidiParserException("Expected to find a system exclusive continue byte.", pos); } //// // Are we in running status? Determine whether we're running and //// // what the current status byte is. bool running; // whether we're in running status if ((nextValue & 0x80) == 0) { //// We're now in running status... if the last status was 0, uh oh! if (status == 0) { throw new MidiParserException("Status byte required for running status.", pos); } //// // Keep the last iteration's status byte, and now we're in running mode running = true; } else { //// // Not running, so store the current status byte and mark running as false status = nextValue; running = false; } return running; } /// /// Parses the data to event. /// /// The delta time. /// The position. /// The status. /// If set to true [running]. /// /// Returns value. /// private MidiEvent ParseDataToEvent(long deltaTime, ref int pos, int status, bool running) { Contract.Requires(this.data != null); MidiEvent tempEvent = null; //// // Grab the 4-bit identifier var messageType = (byte)((status >> 4) & 0xF); //// // Handle voice events if (messageType >= (byte)MidiVoiceMessageType.VoiceNoteOff && messageType <= (byte)MidiVoiceMessageType.PitchBend) { //// 0x8 to 0xE if (!running) { // if we're running, we don't advance; if we're not running, we do pos++; } var channel = (MidiChannel)(byte)(status & 0xF); // grab the channel from the status byte if (pos + 1 < this.data.Length) { tempEvent = ParseVoiceEvent(deltaTime, (MidiVoiceMessageType)messageType, channel, this.data, ref pos); } } else { switch ((MidiCommandCode)status) { case MidiCommandCode.MetaEvent: { //// Handle meta events pos++; byte eventType = 0; if (pos >= 0 && pos < this.data.Length) { eventType = this.data[pos]; } pos++; tempEvent = ParseMetaEvent(deltaTime, eventType, this.data, ref pos); } break; case MidiCommandCode.SystemExclusive: tempEvent = this.ParseSysExEvent(deltaTime, ref pos); break; case MidiCommandCode.EndOfSystemExclusive: tempEvent = this.ParseSysExData(deltaTime, ref pos); break; default: throw new MidiParserException("Invalid status byte found.", pos); } } return tempEvent; } #endregion #region Event Parsing /// /// Parse SysExEvent. /// /// The previously parsed delta-time for this event. /// The position of the start of the event information. /// /// Returns value. /// private MidiEvent ParseSysExEvent(long deltaTime, ref int pos) { Contract.Requires(this.data != null); MidiEvent tempEvent = null; pos++; var length = pos >= 0 && pos < this.data.Length ? ReadVariableLength(this.data, ref pos) : 0; //// If this is single-segment message, process the whole thing if (pos + length - 1 >= 0 && pos + length - 1 < this.data.Length && this.data[pos + length - 1] == (byte)MidiCommandCode.EndOfSystemExclusive && length > 0 && pos >= this.data.GetLowerBound(0)) { this.sysExData = new byte[length - 1]; Array.Copy(this.data, pos, this.sysExData, 0, length - 1); tempEvent = MidiEventSystemExclusive.NewMidiEventSystemExclusive(deltaTime, this.sysExData); } else { //// It's multi-segment, so add the new data to the previously acquired data //// Add to previously acquired sys ex data var oldLength = this.sysExData?.Length ?? 0; if (oldLength + length >= 0 && pos >= this.data.GetLowerBound(0) && pos + length <= this.data.GetLowerBound(0) + this.data.Length) { var newSysExData = new byte[oldLength + length]; this.sysExData?.CopyTo(newSysExData, 0); if (length > 0) { Array.Copy(this.data, pos, newSysExData, oldLength, length); } //// sysExData = newSysExData; Resharper sysExContinue = true; } } pos += length; return tempEvent; } /// /// Parse SysEx Data. /// /// The previously parsed delta-time for this event. /// The position of the start of the event information. /// /// Returns value. /// private MidiEvent ParseSysExData(long deltaTime, ref int pos) { Contract.Requires(this.data != null); if (!sysExContinue) { this.sysExData = null; } //// Figure out how much data there is pos++; long length = pos >= 0 && pos < this.data.Length ? ReadVariableLength(this.data, ref pos) : 0; //// Add to previously acquired sys ex data var oldLength = this.sysExData?.Length ?? 0; if (oldLength + length >= 0) { var newSysExData = new byte[oldLength + length]; this.sysExData?.CopyTo(newSysExData, 0); if (length >= 0) { Array.Copy(this.data, pos, newSysExData, oldLength, (int)length); this.sysExData = newSysExData; } } //// Make it a system message if necessary (i.e. if we find an end marker) if (pos + length - 1 < 0 || this.data[pos + length - 1] != (byte)MidiCommandCode.EndOfSystemExclusive) { return null; } MidiEvent tempEvent = MidiEventSystemExclusive.NewMidiEventSystemExclusive(deltaTime, this.sysExData); //// sysExData = null; Resharper sysExContinue = false; return tempEvent; } #endregion } }